﻿using PoliticalClassifier.Framework;
using PoliticalClassifier.Support;
using System;
using System.Collections.Generic;
using System.Linq;

namespace PoliticalClassifier.Calculations
{
    /// <summary>
    /// PoliticalClassifierCalculate provides an API for calculating simulation results.
    /// </summary>
    public class PoliticalClassifierCalculate
    {
        #region Construction
        /// <summary>
        /// Constructs a calculation object.
        /// </summary>
        /// <param name="votersRecord">The set of voter records.</param>
        /// <param name="weightManager">The weight manager.</param>
        /// <param name="candidateManager">The candidate/party manager.</param>
        public PoliticalClassifierCalculate(ICollection<IVoterRecord> votersRecord, IWeightManager weightManager, ICandidateManager candidateManager)
        {
            m_collectionVoterRecord = votersRecord;
            m_weightManager = weightManager;
            m_candidateManager = candidateManager;
            m_listTransformedPoints = new List<Point2D>();
            m_SocLeftLabelToCoefficientMap = new Dictionary<String, double>();
            m_EconLeftLabelToCoefficientMap = new Dictionary<String, double>();

            InitilizeEquations();
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Compute the simulation results.
        /// </summary>
        /// <returns>The simulation results.</returns>
        public ISimulationResults Compute()
        {
            m_arrayPoint2D = new Point2D[VottersRecord.Count];
            IEnumerator<IVoterRecord>  enumVoterRecord = VottersRecord.GetEnumerator();

            int index = 0;
            Range2D boundingRange = new Range2D(double.MaxValue, double.MinValue, double.MaxValue, double.MinValue);
            while (enumVoterRecord.MoveNext())
            {
                IEnumerator<IVoterAttribute> enumAttributes = enumVoterRecord.Current.Attributes.GetEnumerator();

                while(enumAttributes.MoveNext())
                {
                    TransformedPoints[index].X += (WeightManager[enumAttributes.Current.Name].X * enumAttributes.Current.Value);
                    TransformedPoints[index].Y += (WeightManager[enumAttributes.Current.Name].Y * enumAttributes.Current.Value);
                }

                boundingRange.ExpandToIncludePoint(TransformedPoints[index]);

                index++;
            }

            double scaleFactor = boundingRange.NormalizeRange();
            for (int i = 0; i < TransformedPoints.Length; ++i)
            {
                TransformedPoints[i].X /= scaleFactor;
                TransformedPoints[i].Y /= scaleFactor;
            }

            Point2D overallVoterMedianPoint = CalculateOverallVoterMedianPoint();
            CandidateResultsCollection candidateResults = CalculateCandidateResults(overallVoterMedianPoint);

            return new SimulationResults(overallVoterMedianPoint, (int)Math.Round(scaleFactor), candidateResults);
        }
        #endregion

        #region Public Properties
        /// <summary>
        /// The array of transformed (modeled) voter data.
        /// </summary>
        public Point2D[] TransformedPoints
        {
            get { return m_arrayPoint2D; }
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Initialization routine for equations solved during the simulation.
        /// </summary>
        private void InitilizeEquations()
        {
            foreach (IWeightAttribute weightAttribute in WeightManager)
            {
                SocLeftLabelToCoefficientMap.Add(weightAttribute.Name, weightAttribute.Value.X);
                EconLeftLabelToCoefficientMap.Add(weightAttribute.Name, weightAttribute.Value.Y);
            }
        }

        /// <summary>
        /// Calculates the overall voter median point.
        /// </summary>
        /// <returns>The overall voter median point.</returns>
        private Point2D CalculateOverallVoterMedianPoint()
        {
            if (TransformedPoints.Length == 0)
                return new Point2D(0.0, 0.0);

            bool isCountEven = TransformedPoints.Length % 2 == 0;
            double[] middleXValues = 
                TransformedPoints
                    .Select(point => point.X)
                    .OrderBy(x => x)
                    .Skip(TransformedPoints.Length / 2 - (isCountEven ? 1 : 0))
                    .Take(2)
                    .ToArray();
            double[] middleYValues =
                TransformedPoints
                    .Select(point => point.Y)
                    .OrderBy(y => y)
                    .Skip(TransformedPoints.Length / 2 - (isCountEven ? 1 : 0))
                    .Take(2)
                    .ToArray();

            if (isCountEven)
                return new Point2D((middleXValues[0] + middleXValues[1]) / 2.0, (middleYValues[0] + middleYValues[1]) / 2.0);
            else
                return new Point2D(middleXValues[0], middleYValues[0]);
        }

        /// <summary>
        /// Calculates the results per candidate.
        /// </summary>
        /// <param name="overallVoterMedianPoint">The overall voter median point.</param>
        /// <returns></returns>
        private CandidateResultsCollection CalculateCandidateResults(Point2D overallVoterMedianPoint)
        {
            // Calculate the distance of each voter from each candidate (List of candidates, double[] of voter distances).
            List<CandidateResults> candidateResultsList = new List<CandidateResults>(CandidateManager.Count);
            List<double[]> perCandidateListOfVoterDistances = new List<double[]>();
            foreach (ICandidate candidate in CandidateManager)
            {
                candidateResultsList.Add(new CandidateResults(candidate));

                perCandidateListOfVoterDistances.Add(
                    TransformedPoints
                        .Select(point => new Point2D(point.X - candidate.Location.X, point.Y - candidate.Location.Y))
                        .Select(point => Math.Sqrt(point.X * point.X + point.Y * point.Y))
                        .ToArray()
                    );
            }

            // Calculate the number of votes per candidate.
            for (int voterIndex = 0; voterIndex < TransformedPoints.Length; ++voterIndex)
            {
                Tuple<int, double> candidateIndexMinValuePair = Tuple.Create(0, perCandidateListOfVoterDistances[0][voterIndex]);
                for (int candidateIndex = 1; candidateIndex < perCandidateListOfVoterDistances.Count; ++candidateIndex)
                {
                    if (perCandidateListOfVoterDistances[candidateIndex][voterIndex] < candidateIndexMinValuePair.Item2)
                        candidateIndexMinValuePair = Tuple.Create(candidateIndex, perCandidateListOfVoterDistances[candidateIndex][voterIndex]);
                }

                candidateResultsList[candidateIndexMinValuePair.Item1].NumberOfVotes++;
            }

            // Calculate the distance from the overall voter median.
            foreach(CandidateResults candidateResults in candidateResultsList)
            {
                Point2D vector = new Point2D(
                    candidateResults.Candidate.Location.X - overallVoterMedianPoint.X, 
                    candidateResults.Candidate.Location.Y - overallVoterMedianPoint.Y);
                candidateResults.DistanceFromMedian = Math.Sqrt(vector.X * vector.X + vector.Y * vector.Y);
            }

            return new CandidateResultsCollection(candidateResultsList.Cast<ICandidateResults>().ToList());
        }
        #endregion

        #region Private Properties
        /// <summary>
        /// The collection of voter records.
        /// </summary>
        private ICollection<IVoterRecord> VottersRecord
        {
            get { return m_collectionVoterRecord; }
        }

        /// <summary>
        /// The weight manager.
        /// </summary>
        private IWeightManager WeightManager
        {
            get { return m_weightManager; }
        }

        /// <summary>
        /// The candidate manager.
        /// </summary>
        private ICandidateManager CandidateManager
        {
            get { return m_candidateManager; }
        }

        /// <summary>
        /// The coefficient map for the social axis.
        /// </summary>
        private IDictionary<String, double> SocLeftLabelToCoefficientMap
        {
            get { return m_SocLeftLabelToCoefficientMap; } 
        }

        /// <summary>
        /// The coefficient map for the economic axis.
        /// </summary>
        private IDictionary<String, double> EconLeftLabelToCoefficientMap
        {
            get { return m_EconLeftLabelToCoefficientMap; } 
        }
        #endregion

        #region Private Fields
        private ICollection<IVoterRecord> m_collectionVoterRecord;
        private IWeightManager m_weightManager;
        private ICandidateManager m_candidateManager;
        private IList<Point2D> m_listTransformedPoints;
        private Point2D [] m_arrayPoint2D;
        private IDictionary<String, double> m_SocLeftLabelToCoefficientMap;
        private IDictionary<String, double> m_EconLeftLabelToCoefficientMap;
        #endregion
    }
}
